package org.xtremeobjects.domain.states;

import java.util.ArrayList;
import java.util.List;

import org.xtremeobjects.domain.model.expression.el.ExpressionEvaluator;
import org.xtremeobjects.domain.states.Action;
import org.xtremeobjects.domain.states.Event;
import org.xtremeobjects.domain.states.State;
import org.xtremeobjects.domain.states.StateMachine;

/**
 * @author    Juan
 */
public class StateMachineRuntime {

	private StateMachine stateMachine;
	private State currentState;
	private Object object;
	private List<StateMachineRuntimeListener> listeners;
	
	StateMachineRuntime(StateMachine definition) {
		stateMachine = definition;
		reset();
	}

	/**
	 * Reset state machine. 
	 * It puts the state machine into the initial state.
	 *
	 */
	public void reset() {
		currentState = stateMachine.start().getNextState();
	}

	public State getCurrentState() {
		return currentState;
	}
	
	public void recoverStateFromPersistence( State st ) {
		currentState = st;
	}

	public void handleEvent(String eventName) {
		System.out.println("handleEvent(inState=" + currentState.getId()+", event=" + eventName+")");
		Event event = stateMachine.findEventByName(eventName);

		// Not an event defined for the state machine
		if(event==null) {
			System.out.println("StateMachineInstance.handleEvent(): Undeclared event " + eventName + " for " + stateMachine.getName() + " state machine");
			return; // TODO check if we need to throw exception
		}

		// in case is a valid event notify listeners
		notifyOnEvent(event);
		
		
		/** 
		 * get transitions from the current state that fire when
		 * this event cames in this event.
		 */ 
		List<Transition> transitions = getCurrentState().getTransitionsForEvent(event);
		
		// Add transitions from any state too
		transitions.addAll(getStateMachine().ANY.getTransitionsForEvent(event));
		
		transitions = evaluateGuards(transitions);
		
		if(transitions.size()==0)
			throw new RuntimeException("Invalid event: " + event.getId() + " at state " + currentState.getId());
		
		/**
		 * check which guard conditions apply TODO
		 */
		
		if(transitions.size()!=1)
			throw new RuntimeException("Invalid state machine definition, multiple state transitions valid for event: " + event.getId() + " at state " + currentState.getId());

		/**
		 * Do transition:
		 * Execute exit actions for current state
		 * change state
		 * Execute entry actions for new state
		 */
		State oldState = currentState;
		State newState = transitions.get(0).getTo(); 
		executeAction( currentState, State.EXIT );
		notifyStateChange( oldState, newState );
		currentState = newState;
		System.out.println("stateTransition(from=" +oldState.getId()+", to=" + currentState.getId()+")");
		executeAction( currentState, State.ENTRY );
	}

	private void notifyStateChange(State oldState, State newState) {
		for (StateMachineRuntimeListener lst : getListeners()) {
			lst.onStateChange(this, oldState, newState);
		}
	}

	private void notifyOnEvent(Event event) {
		for (StateMachineRuntimeListener lst : getListeners()) {
			lst.onEvent(this, event);
		}
	}

	private List<Transition> evaluateGuards(List<Transition> transitions) {
		List<Transition> newTransitions = new ArrayList<Transition>();
		for (Transition transition : transitions) {
			if(transition.getGuard()!=null && transition.getGuard().getExpression()!=null) {
				Object ret = new ExpressionEvaluator().evaluate(this.getContextObject(), "${" + transition.getGuard().getExpression() + "}");
				System.out.println("evaluateTransition(from="+transition.getFrom().getId()+", to="+transition.getTo().getId()+", guard="+transition.getGuard().getExpression() + ") = " + ret);
				if( ((Boolean)ret)==true) {
						newTransitions.add(transition);
				}
			} else
				newTransitions.add(transition);
		}
		return newTransitions;
	}

	private void executeAction(State state, Event event) {
		/**
		 * 
		 */
		for (Action action : state.getActionsForEvent(event)) {
			System.out.println("executeAction(" +state.getId()+"): " + action.getText() );
			notifyOnActionExecution(action);
			new ExpressionEvaluator().evaluate(getContextObject(), "#{" + action.getText() + "}");
		}
	}

	private void notifyOnActionExecution(Action action) {
		for (StateMachineRuntimeListener lst : getListeners()) {
			lst.onActionExecution(this, action);
		}
	}

	public void setContextObject(Object object) {
		this.object = object;
	}

	public Object getContextObject() {
		return object;
	}

	public StateMachine getStateMachine() {
		return stateMachine;
	}

	public void addListener(StateMachineRuntimeListener listener) {
		getListeners().add( listener );
	}
	
	public void removeListener(StateMachineRuntimeListener listener) {
		getListeners().remove( listener );
	}

	private List<StateMachineRuntimeListener> getListeners() {
		if(listeners==null)
			listeners = new ArrayList<StateMachineRuntimeListener>();
		return listeners;
	}
}
